package com.activeharmony.util;

/*
 * Copyright (C) 2010 Michael Pardo
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import com.activeharmony.Cache;
import com.activeharmony.Model;
import com.activeharmony.TableInfo;
import com.activeharmony.annotation.Column;
import com.activeharmony.serializer.TypeSerializer;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ohos.data.resultset.ResultSet;

/**
 * SQLiteUtils class
 */
public final class SQLiteUtils {
    // ////////////////////////////////////////////////////////////////////////////////////
    // ENUMERATIONS
    // ////////////////////////////////////////////////////////////////////////////////////

    /**
     * SQLiteType enum
     */
    public enum SQLiteType {
        INTEGER, REAL, TEXT, BLOB
    }

    // ////////////////////////////////////////////////////////////////////////////////////
    // PUBLIC CONSTANTS
    // ////////////////////////////////////////////////////////////////////////////////////

    /**
     * FOREIGN_KEYS_SUPPORTED
     */
    public static final boolean FOREIGN_KEYS_SUPPORTED = true;

    // ////////////////////////////////////////////////////////////////////////////////////
    // PRIVATE CONTSANTS
    // ////////////////////////////////////////////////////////////////////////////////////

    @SuppressWarnings("serial")
    private static final HashMap<Class<?>, SQLiteType> TYPE_MAP = new HashMap<Class<?>, SQLiteType>() {
        {
            put(byte.class, SQLiteType.INTEGER);
            put(short.class, SQLiteType.INTEGER);
            put(int.class, SQLiteType.INTEGER);
            put(long.class, SQLiteType.INTEGER);
            put(float.class, SQLiteType.REAL);
            put(double.class, SQLiteType.REAL);
            put(boolean.class, SQLiteType.INTEGER);
            put(char.class, SQLiteType.TEXT);
            put(byte[].class, SQLiteType.BLOB);
            put(Byte.class, SQLiteType.INTEGER);
            put(Short.class, SQLiteType.INTEGER);
            put(Integer.class, SQLiteType.INTEGER);
            put(Long.class, SQLiteType.INTEGER);
            put(Float.class, SQLiteType.REAL);
            put(Double.class, SQLiteType.REAL);
            put(Boolean.class, SQLiteType.INTEGER);
            put(Character.class, SQLiteType.TEXT);
            put(String.class, SQLiteType.TEXT);
            put(Byte[].class, SQLiteType.BLOB);
        }
    };

    // ////////////////////////////////////////////////////////////////////////////////////
    // PRIVATE MEMBERS
    // ////////////////////////////////////////////////////////////////////////////////////

    private static HashMap<String, List<String>> sIndexGroupMap;
    private static HashMap<String, List<String>> sUniqueGroupMap;
    private static HashMap<String, Column.ConflictAction> sOnUniqueConflictsMap;

    // ////////////////////////////////////////////////////////////////////////////////////
    // PUBLIC METHODS
    // ////////////////////////////////////////////////////////////////////////////////////

    /**
     * execSql
     *
     * @param sql sql
     */
    public static void execSql(String sql) {
        Cache.openDatabase().execSQL(sql);
    }

    /**
     * execSql
     *
     * @param sql sql
     * @param bindArgs bindArgs
     */
    public static void execSql(String sql, Object[] bindArgs) {
        Cache.openDatabase().execSQL(sql, bindArgs);
    }

    /**
     * rawQuery
     *
     * @param type type
     * @param sql sql
     * @param selectionArgs selectionArgs
     * @return list of Model
     */
    public static <T extends Model> List<T> rawQuery(Class<? extends Model> type, String sql, String[] selectionArgs) {
        ResultSet cursor = Cache.openDatabase().rawQuery(sql, selectionArgs);
        List<T> entities = processCursor(type, cursor);
        cursor.close();

        return entities;
    }

    /**
     * intQuery
     *
     * @param sql sql
     * @param selectionArgs selectionArgs
     * @return number
     */
    public static int intQuery(final String sql, final String[] selectionArgs) {
        final ResultSet cursor = Cache.openDatabase().rawQuery(sql, selectionArgs);
        final int number = processIntCursor(cursor);
        cursor.close();

        return number;
    }

    /**
     * rawQuerySingle
     *
     * @param type type
     * @param selectionArgs selectionArgs
     * @param sql sql
     * @return Model
     */
    public static <T extends Model> T rawQuerySingle(Class<? extends Model> type, String sql, String[] selectionArgs) {
        List<T> entities = rawQuery(type, sql, selectionArgs);

        if (entities.size() > 0) {
            return entities.get(0);
        }

        return null;
    }

    // Database creation
    /**
     * createUniqueDefinition
     *
     * @param tableInfo tableInfo
     * @return definitions
     */
    public static ArrayList<String> createUniqueDefinition(TableInfo tableInfo) {
        final ArrayList<String> definitions = new ArrayList<String>();
        sUniqueGroupMap = new HashMap<String, List<String>>();
        sOnUniqueConflictsMap = new HashMap<String, Column.ConflictAction>();

        for (Field field : tableInfo.getFields()) {
            createUniqueColumnDefinition(tableInfo, field);
        }

        if (sUniqueGroupMap.isEmpty()) {
            return definitions;
        }

        Set<String> keySet = sUniqueGroupMap.keySet();
        for (String key : keySet) {
            List<String> group = sUniqueGroupMap.get(key);
            Column.ConflictAction conflictAction = sOnUniqueConflictsMap.get(key);

            definitions.add(String.format("UNIQUE (%s) ON CONFLICT %s",
                String.join(", ", group), conflictAction.toString()));
        }

        return definitions;
    }

    /**
     * createUniqueColumnDefinition
     *
     * @param tableInfo tableInfo
     * @param field field
     */
    public static void createUniqueColumnDefinition(TableInfo tableInfo, Field field) {
        final String name = tableInfo.getColumnName(field);
        final Column column = field.getAnnotation(Column.class);

        if ("mId".equals(field.getName())) {
            return;
        }

        String[] groups = column.uniqueGroups();
        Column.ConflictAction[] conflictActions = column.onUniqueConflicts();
        if (groups.length != conflictActions.length) {
            return;
        }

        for (int i = 0; i < groups.length; i++) {
            String group = groups[i];
            Column.ConflictAction conflictAction = conflictActions[i];

            if (group.isEmpty()) {
                continue;
            }

            List<String> list = sUniqueGroupMap.get(group);
            if (list == null) {
                list = new ArrayList<String>();
            }
            list.add(name);

            sUniqueGroupMap.put(group, list);
            sOnUniqueConflictsMap.put(group, conflictAction);
        }
    }

    /**
     * createIndexDefinition
     *
     * @param tableInfo tableInfo
     * @return definitions
     */
    public static String[] createIndexDefinition(TableInfo tableInfo) {
        final ArrayList<String> definitions = new ArrayList<String>();
        sIndexGroupMap = new HashMap<String, List<String>>();

        for (Field field : tableInfo.getFields()) {
            createIndexColumnDefinition(tableInfo, field);
        }

        if (sIndexGroupMap.isEmpty()) {
            return new String[0];
        }

        for (Map.Entry<String, List<String>> entry : sIndexGroupMap.entrySet()) {
            definitions.add(String.format("CREATE INDEX IF NOT EXISTS %s on %s(%s);",
                "index_" + tableInfo.getTableName() + "_" + entry.getKey(),
                tableInfo.getTableName(), String.join(", ", entry.getValue())));
        }

        return definitions.toArray(new String[definitions.size()]);
    }

    /**
     * createIndexColumnDefinition
     *
     * @param tableInfo tableInfo
     * @param field field
     */
    public static void createIndexColumnDefinition(TableInfo tableInfo, Field field) {
        final String name = tableInfo.getColumnName(field);
        final Column column = field.getAnnotation(Column.class);

        if ("mId".equals(field.getName())) {
            return;
        }

        if (column.index()) {
            List<String> list = new ArrayList<String>();
            list.add(name);
            sIndexGroupMap.put(name, list);
        }

        String[] groups = column.indexGroups();
        for (String group : groups) {
            if (group.isEmpty()) {
                continue;
            }

            List<String> list = sIndexGroupMap.get(group);
            if (list == null) {
                list = new ArrayList<String>();
            }

            list.add(name);
            sIndexGroupMap.put(group, list);
        }
    }

    /**
     * createTableDefinition
     *
     * @param tableInfo tableInfo
     * @return String definition
     */
    public static String createTableDefinition(TableInfo tableInfo) {
        final ArrayList<String> definitions = new ArrayList<String>();

        for (Field field : tableInfo.getFields()) {
            String definition = createColumnDefinition(tableInfo, field);
            if (!definition.isEmpty()) {
                definitions.add(definition);
            }
        }

        definitions.addAll(createUniqueDefinition(tableInfo));

        return String.format("CREATE TABLE IF NOT EXISTS %s (%s);", tableInfo.getTableName(),
            String.join(", ", definitions));
    }

    @SuppressWarnings("unchecked")
    /**
     * createColumnDefinition
     *
     * @param tableInfo tableInfo
     * @param field field
     * @return String definition
     */
    public static String createColumnDefinition(TableInfo tableInfo, Field field) {
        StringBuilder definition = new StringBuilder();

        Class<?> type = field.getType();
        final String name = tableInfo.getColumnName(field);
        final TypeSerializer typeSerializer = Cache.getParserForType(field.getType());
        final Column column = field.getAnnotation(Column.class);

        if (typeSerializer != null) {
            type = typeSerializer.getSerializedType();
        }
        if (TYPE_MAP.containsKey(type)) {
            definition.append(name);
            definition.append(" ");
            definition.append(TYPE_MAP.get(type).toString());
        } else if (ReflectionUtils.isModel(type)) {
            definition.append(name);
            definition.append(" ");
            definition.append(SQLiteType.INTEGER.toString());
        } else if (ReflectionUtils.isSubclassOf(type, Enum.class)) {
            definition.append(name);
            definition.append(" ");
            definition.append(SQLiteType.TEXT.toString());
        } else {
            Log.i("createColumnDefinition ReflectionUtils is not a subclass of Enum");
        }
        if (!definition.toString().isEmpty()) {
            if (name.equals(tableInfo.getIdName())) {
                definition.append(" PRIMARY KEY AUTOINCREMENT");
            } else if (column != null) {
                if (column.length() > -1) {
                    definition.append("(");
                    definition.append(column.length());
                    definition.append(")");
                }
                if (column.notNull()) {
                    definition.append(" NOT NULL ON CONFLICT ");
                    definition.append(column.onNullConflict().toString());
                }
                if (column.unique()) {
                    definition.append(" UNIQUE ON CONFLICT ");
                    definition.append(column.onUniqueConflict().toString());
                }
            } else {
                Log.i("createColumnDefinition column is null");
            }
            if (FOREIGN_KEYS_SUPPORTED && ReflectionUtils.isModel(type)) {
                definition.append(" REFERENCES ");
                definition.append(Cache.getTableInfo((Class<? extends Model>) type).getTableName());
                definition.append("(" + tableInfo.getIdName() + ")");
                definition.append(" ON DELETE ");
                definition.append(column.onDelete().toString().replace("_", " "));
                definition.append(" ON UPDATE ");
                definition.append(column.onUpdate().toString().replace("_", " "));
            }
        } else {
            Log.e("No type mapping for: " + type.toString());
        }
        return definition.toString();
    }

    @SuppressWarnings("unchecked")
    /**
     * processCursor
     *
     * @param type type
     * @param cursor cursor
     * @return array list
     */
    public static <T extends Model> List<T> processCursor(Class<? extends Model> type, ResultSet cursor) {
        TableInfo tableInfo = Cache.getTableInfo(type);
        String idName = tableInfo.getIdName();
        final List<T> entities = new ArrayList<T>();

        try {
            Constructor<?> entityConstructor = type.getConstructor();

            if (cursor.goToFirstRow()) {
                /**
                 * Obtain the columns ordered to fix issue #106
                 * when the cursor have multiple columns with same name obtained from join tables.
                 */
                List<String> columnsOrdered = new ArrayList<String>(Arrays.asList(cursor.getAllColumnNames()));
                do {
                    Model entity = Cache.getEntity(type, cursor.getLong(columnsOrdered.indexOf(idName)));
                    if (entity == null) {
                        entity = (T) entityConstructor.newInstance();
                    }

                    entity.loadFromCursor(cursor);
                    entities.add((T) entity);
                }
                while (cursor.goToNextRow());
            }

        } catch (NoSuchMethodException e) {
            throw new RuntimeException(
                "Your model " + type.getName() + " does not define a default " +
                    "constructor. The default constructor is required for " +
                    "now in ActiveHarmony models, as the process to " +
                    "populate the ORM model is : " +
                    "1. instantiate default model " +
                    "2. populate fields"
            );
        } catch (Exception e) {
            Log.e("Failed to process cursor.", e);
        }

        return entities;
    }

    private static int processIntCursor(final ResultSet cursor) {
        if (cursor.goToFirstRow()) {
            return cursor.getInt(0);
        }
        return 0;
    }

    /**
     * lexSqlScript
     *
     * @param sqlScript sqlScript
     * @return String list
     */
    public static List<String> lexSqlScript(String sqlScript) {
        ArrayList<String> sl = new ArrayList<String>();
        boolean inString = false;
        boolean quoteNext = false;
        StringBuilder stringBuilder = new StringBuilder(100);

        for (int i = 0; i < sqlScript.length(); i++) {
            char character = sqlScript.charAt(i);

            if (character == ';' && !inString && !quoteNext) {
                sl.add(stringBuilder.toString());
                stringBuilder = new StringBuilder(100);
                inString = false;
                quoteNext = false;
                continue;
            }

            if (character == '\'' && !quoteNext) {
                inString = !inString;
            }

            quoteNext = character == '\\' && !quoteNext;

            stringBuilder.append(character);
        }

        if (stringBuilder.length() > 0) {
            sl.add(stringBuilder.toString());
        }

        return sl;
    }
}
